/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkAssignStmt, ArkClass, ArkFile, ArkInstanceFieldRef, ArkInvokeStmt, ArkMethod, ClassType, DEFAULT_ARK_CLASS_NAME, FieldSignature, FunctionType, Local, Stmt, Value } from 'arkanalyzer/lib';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { Rule, Defects, MethodMatcher, MatcherTypes, MatcherCallback, CheckerUtils } from '../../Index';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'UseTransitionToReplaceAnimatetoCheck');
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/use-transition-to-replace-animateto-check.md',
    description: 'It is recommended that you use transitions for component transitions.'
};
const animateToSiganture = `@ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.animateTo(@ohosSdk/component/common.d.ts: AnimateParam, @ohosSdk/component/common.d.ts: ${DEFAULT_ARK_CLASS_NAME}.%AM0())`;
const opacitySiganture: string[] = [
    `@ohosSdk/component/common.d.ts: CommonMethod.opacity(number|Resource)`,
    `@ohosSdk/component/common.d.ts: CommonMethod.opacity(number|@ohosSdk/component/units.d.ts: ${DEFAULT_ARK_CLASS_NAME}.[static]${DEFAULT_ARK_CLASS_NAME}()#Resource)`
];
let stateFieldSet: Set<FieldSignature> = new Set();

export class UseTransitionToReplaceAnimatetoCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private methodMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD
    };

    public registerMatchers(): MatcherCallback[] {
        const matchMethodCb: MatcherCallback = {
            matcher: this.methodMatcher,
            callback: this.check
        };
        return [matchMethodCb];
    }

    public check = (arkMethod: ArkMethod): void => {
        let stmts = arkMethod.getBody()?.getCfg().getStmts();
        if (!stmts) {
            return;
        }
        for (const stmt of stmts) {
            let invoke = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invoke) {
                continue;
            }
            if (invoke.getMethodSignature().toString() !== animateToSiganture) {
                continue;
            }
            let args = invoke.getArgs();
            this.processArgs(args, arkMethod);
            if (this.checkOpacitySignature(arkMethod.getDeclaringArkClass())) {
                this.reportIssue(arkMethod.getDeclaringArkFile(), stmt, invoke.getMethodSignature().getMethodSubSignature().getMethodName());
            }
        }
    };

    private checkOpacitySignature(arkClass: ArkClass): boolean {
        for (let method of arkClass.getMethods()) {
            let stmts = method.getBody()?.getCfg().getStmts();
            if (!stmts) {
                return false;
            }
            for (const stmt of stmts) {
                let invoke = CheckerUtils.getInvokeExprFromStmt(stmt);
                if (!invoke) {
                    continue;
                }
                if (!opacitySiganture.includes(invoke.getMethodSignature().toString())) {
                    continue;
                }
                let arg = invoke.getArg(0);
                if (!(arg instanceof Local)) {
                    continue;
                }
                let decStmt = arg.getDeclaringStmt();
                if (!decStmt) {
                    continue;
                }
                if (!(decStmt instanceof ArkAssignStmt)) {
                    continue;
                }
                let rightOp = decStmt.getRightOp();
                if (!(rightOp instanceof ArkInstanceFieldRef)) {
                    continue;
                }
                if (stateFieldSet.has(rightOp.getFieldSignature())) {
                    return true;
                }
            }
        }
        return false;
    }

    private processArgs(args: Value[], arkMethod: ArkMethod): void {
        for (const arg of args) {
            if (!(arg instanceof Local)) {
                continue;
            }
            let type = arg.getType();
            if (type instanceof ClassType) {
                let classSignature = type.getClassSignature();
                let arkClass = arkMethod.getDeclaringArkFile().getClass(classSignature);
                if (!arkClass) {
                    return;
                }
                for (let method of arkClass.getMethods()) {
                    this.processMethodStatements(method, arkMethod.getDeclaringArkClass());
                }
            } else if (type instanceof FunctionType) {
                let functionSignature = type.getMethodSignature();
                let method = arkMethod.getDeclaringArkClass().getMethod(functionSignature);
                if (!method) {
                    continue;
                }
                this.processMethodStatements(method, arkMethod.getDeclaringArkClass());
            }
        }
    }

    private processMethodStatements(method: ArkMethod, arkClass: ArkClass): void {
        for (let stmt of method.getBody()?.getCfg().getStmts() ?? []) {
            if (stmt instanceof ArkInvokeStmt) {
                let invmethod = arkClass.getDeclaringArkFile().getScene().getMethod(stmt.getInvokeExpr().getMethodSignature());
                if (invmethod === null) {
                    continue;
                }
                this.processMethodStatements(invmethod, arkClass);
            } else if (stmt instanceof ArkAssignStmt) {
                let leftOp = stmt.getLeftOp();
                if (!(leftOp instanceof ArkInstanceFieldRef)) {
                    continue;
                }
                let field = arkClass.getField(leftOp.getFieldSignature());
                if (field && field.hasDecorator('State')) {
                    stateFieldSet.add(leftOp.getFieldSignature());
                }
            }
        }
    }

    private reportIssue(arkFile: ArkFile, stmt: Stmt, methodName: string): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const originPositionInfo = stmt.getOriginPositionInfo();
        const lineNum = originPositionInfo.getLineNo();
        const text = stmt.getOriginalText();
        if (!text || text.length === 0) {
            return;
        }
        const startColumn = originPositionInfo.getColNo() + text.lastIndexOf(methodName);
        const endColunm = startColumn + methodName.length;
        let defects = new Defects(lineNum, startColumn, endColunm, this.metaData.description, severity, this.rule.ruleId,
            filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}